1 module modular_db.actions;
2 
3 import std.exception: enforce;
4 
5 import modular_db.database;
6 import modular_db.exceptions;
7 import modular_db.module_;
8 import modular_db.module_qualification;
9 
10 private @system:
11 
12 public enum Mode: ubyte {
13     load,
14     setup,
15     migrate,
16 }
17 
18 ResultRange _queryModuleInfo(Database db, string sql, string moduleUrl) {
19     try
20         return db.execute(sql, moduleUrl);
21     catch (SqliteException)
22         throw new UninitializedDbException;
23 }
24 
25 public long getModuleId(Database db, ModuleQualification q, string moduleUrl) {
26     auto moduleInfo = _queryModuleInfo(db, q.format!`
27         SELECT oid
28         FROM [-|0modules]
29         WHERE url = ?
30     `, moduleUrl);
31     enforce(!moduleInfo.empty, new ModuleNotFoundException(moduleUrl));
32     return moduleInfo.oneValue!long;
33 }
34 
35 auto _loadModule(L)(Database db, ref L loader, Mode mode, ModuleQualification q) {
36     const moduleUrl = loader.url;
37     const moduleVersion = loader.version_;
38 
39     auto transaction = db.startTransaction();
40     scope(success) transaction.commit();
41     auto moduleInfo = _queryModuleInfo(db, q.format!`
42         SELECT oid, version
43         FROM [-|0modules]
44         WHERE url = ?
45     `, moduleUrl);
46     if (moduleInfo.empty) {
47         // No such module yet.
48         enforce(mode != Mode.load, new ModuleNotFoundException(moduleUrl));
49         db.execute(q.format!`
50             INSERT INTO [-|0modules](url, version)
51             VALUES (?, ?)
52         `, moduleUrl, moduleVersion);
53         q._id = db.lastInsertRowid;
54         return loader.setup(db, q);
55     }
56     // Module exists.
57     const moduleId = moduleInfo.front.peek!long(0);
58     const storedVersion = moduleInfo.front.peek!long(1);
59     moduleInfo = typeof(moduleInfo).init;
60     if (storedVersion == moduleVersion) {
61         q._id = moduleId;
62         return loader.load(db, q);
63     }
64     // Needs migration.
65     enforce(mode == Mode.migrate && storedVersion < moduleVersion,
66         new InvalidModuleVersionException(moduleUrl, moduleVersion, storedVersion),
67     );
68     db.execute(q.format!`
69         UPDATE [-|0modules]
70         SET version = ?
71         WHERE oid = ?
72     `, moduleVersion, moduleId);
73     q._id = moduleId;
74     return loader.migrate(db, q, storedVersion);
75 }
76 
77 // We do not constrain these functions with `isModuleLoader!L`, so that we get better error
78 // messages.
79 public ModuleLoaderModuleType!L loadModule(L)(
80     Database db, ref L loader, Mode mode = Mode.load, string schema = "main",
81 ) {
82     return _loadModule(db, loader, mode, ModuleQualification(schema, 0L));
83 }
84 
85 public ModuleLoaderModuleType!L loadModule(L)(
86     Database db, Mode mode = Mode.load, string schema = "main",
87 ) {
88     L loader;
89     return loadModule(db, loader, mode, schema);
90 }
91 
92 public void initialize(Database db, Mode mode = Mode.load, string schema = "main") {
93     import modular_db.module_module;
94 
95     const q = ModuleQualification(schema, 0L);
96     ModuleModuleLoader loader;
97     if (mode == Mode.load)
98         _loadModule(db, loader, mode, q);
99     else
100         try
101             _loadModule(db, loader, mode, q);
102         catch (UninitializedDbException) {
103             auto transaction = db.startTransaction();
104             scope(success) transaction.commit();
105             loader.setup(db, q);
106         }
107 }
108 
109 public void dropModule(Database db, string moduleUrl, string schema = "main") {
110     import std.algorithm.iteration: map;
111     import std.array: appender, array;
112     import std.conv: toChars;
113     import std.format: sformat;
114     import std.typecons: tuple;
115 
116     import d2sqlite3.results: PeekMode;
117 
118     const q = ModuleQualification(schema, 0L);
119     const moduleId = getModuleId(db, q, moduleUrl);
120 
121     const nested = db.inTransaction;
122     const withForeignKeys = db.execute("PRAGMA foreign_keys").oneValue!bool;
123     if (withForeignKeys) {
124         enforce!DbException(!nested,
125             "Cannot drop a module in a transaction with enabled `foreign_keys`",
126         );
127         db.execute("PRAGMA foreign_keys = OFF");
128     }
129     scope(exit)
130         if (withForeignKeys)
131             db.execute("PRAGMA foreign_keys = ON");
132     auto transaction = Transaction(db.raw, nested);
133     scope(success) transaction.commit();
134 
135     db.execute(q.format!`
136         DELETE FROM [-|0modules]
137         WHERE oid = ?
138     `, moduleId);
139 
140     char[long.min.toChars().length + 7] buffer = void;
141     auto name = appender!(char[ ]);
142     name.reserve(32);
143     foreach (entity;
144         db.execute(
145             q.format!`
146                 SELECT type, name
147                 FROM [-|sqlite_master]
148                 WHERE name GLOB ?
149             `,
150             buffer[ ].sformat!"%s[^0-9]*"(moduleId),
151         ).map!((row) {
152             name.clear();
153             foreach (c; row.peek!(string, PeekMode.slice)(1))
154                 if (c != '"')
155                     name ~= c;
156                 else
157                     name ~= `""`;
158             return tuple!(q{type}, q{name})(row.peek!string(0), name.data.idup);
159         }).array()
160     )
161         db.execute(q.format!`DROP %1$s [-|%2$s]`(entity.type, entity.name));
162 }